feat(plugin-security): walled bootstrap stops minting the platform-admin grant row; platformAdmin audit service; legacy-grant deprecation pointer (L4) - #13514
Conversation
…min grant row; platformAdmin audit service; legacy-grant deprecation pointer Under walled postures bootstrapPlatformAdmin writes no sys_user_permission_set row any more - standing is config-derived at the one derivation site (resolve-authz-context 6b-config). single keeps first-user promotion and its grant row byte-for-byte (Choice 4A). The walled branch now reports: a per-boot standing log (one implementation with the new read-only platformAdmin service: configuredEmails() + standing()), and a once-per-process deprecation pointer for a detected legacy org-less grant, through the same latch the derivation- site reporter uses, naming OS_PLATFORM_OWNER_EMAIL and the holder. shouldReplayBootstrapFor narrows with the retired elevation: sys_user insert/create under non-walled postures only - the #11343 update arm existed solely to re-attempt the walled elevation after the verifying write, and under walled postures no sys_user write can change the bootstrap's answer at all. The local isEmailVerified alias is gone; the shared predicates (isEmailVerifiedUserRow, normalizePlatformAdminEmail) are consumed directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
📓 Docs Drift CheckThis PR changes 1 package(s): 29 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9fb9e8d89cf2707c3b7f29eac7ca0814f9f7ee1f && git checkout 9fb9e8d89cf2707c3b7f29eac7ca0814f9f7ee1f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e7191ce71da1a8cc25c05e3d372eb4f63be5517a d5eeda5e408cd10391e48f7a1613c39c7d640f9a && git checkout -B drift-repro e7191ce71da1a8cc25c05e3d372eb4f63be5517a && git merge --no-ff d5eeda5e408cd10391e48f7a1613c39c7d640f9a
node scripts/docs-audit/affected-docs.mjs --json e7191ce71da1a8cc25c05e3d372eb4f63be5517a
|
Fixes #11974
Part of #11663 (leg L4; design comment 5394453215 §6 row L4, maintainer acceptance 5404675670, bundle 1A/2B/3A/4A/5A/6A/7A — verbatim 「接受你的建议,继续」). #11973 (L3) remains open and is not addressed here; #11670 remains open.
What lands
Union of gates/tests re-run at the final commit
d5eeda5— all green (15 derived gate families incl.check:nul-bytes, plugin-security 90 test files / 1664 tests, package typecheck; downstream: verify harness posture suites 18/18, walled dogfood fixtures 18 passed / 3 env-skipped).bootstrapPlatformAdminundergroup/isolatedwrites nosys_user_permission_setrow, whatever accounts exist. Standing is config-derived at the one derivation site (resolve-authz-context.ts§6b-config, landed with L2/PR feat(core): anchor PLATFORM_ADMIN on a verified OS_PLATFORM_OWNER_EMAIL match, inside the one derivation site #13146 and fix(core,plugin-auth,plugin-security): every OS_PLATFORM_OWNER_EMAIL reader asks the ONE list-aware parser #13319) — each declared, verified account resolvesPLATFORM_ADMINat request time.singlekeeps first-user promotion and its grant row byte-for-byte (Choice 4A; over-denial pins kept).platformAdminservice (newplatform-admin-service.ts, registered by SecurityPlugin):configuredEmails()(declared/refused/normalized list) +standing()(per entry: registered, verified, standing-holding user id). Frozen; no writable member (Choice 3A: no runtime mutation path). The walled bootstrap's per-boot standing log consumes the same implementation, so log and service cannot disagree.reportLegacyPlatformAdminGrant— the SAME once-per-process latch the derivation-site reporter uses, so boot-time + request-time detection can never total two lines. Pinned: a seeded legacy grant produces exactly one line namingOS_PLATFORM_OWNER_EMAIL, the holder id and their address, across repeated bootstraps. Nothing is revoked (P5's honoured window).PLATFORM_ADMINthroughbuildContextForUser(which delegates to the one resolver; core's own §6b-config pins cover the derivation side).shouldReplayBootstrapFornow fires only forsys_userinsert/create under non-walled postures. The [security] 围墙提权只按未验证邮箱匹配 —— #11184 落地后仍存在一条更窄的同形路径:抢在 owner 之前用其邮箱注册即可被提权 #11343 update arm (email_verified/email) existed solely to re-attempt the walled elevation after the verifying write; that elevation is retired, and under walled postures nosys_userwrite can change the bootstrap's answer. Middleware-seam pin re-derived accordingly.isEmailVerifiedalias is deleted (the predicate's canonical home is@objectstack/types, consumed via the shared standing implementation — the card's "promoted into core" premise was stale; nothing inpackages/typesorpackages/coreis touched).Guard refusals: none deleted, none added
L3's
last-admin-guardre-pricing is its own reviewed step and is untouched (as is all ofpackages/plugins/plugin-auth/**). The walledwalled_owner_email_undeclaredbackstop refusal is kept with its exact reason string;walled_owner_not_registered/walled_owner_not_verifiedare not refusals removed but elevation outcomes whose decision moved to the derivation site — their distinctions survive as the standing report's per-entryregistered/verifiedfields, and the fail-closed direction (unverified confers nothing) is enforced and pinned in core.Behavior notes for the contract reviewer
walled_config_derived(waswalled_owner_not_registered/walled_owner_not_verified, or a grant write).already_have_adminis now asingle-only outcome; under walled a pre-existing human grant row gets the deprecation pointer instead of a silent early exit. The only in-repo consumer reading the report (cli meta resync) reads seed/resync counters only — unaffected.claimSeedOwnership) no longer runs under walled postures — it was keyed on the promoted admin, and there is none. Walled/multi-org seed ownership is the enterprise organizations package's per-org bootstrap job (perauth-plugin.ts's posture split).singlehandoff unchanged.3800e42): plugin-auth'sensureDefaultOrganizationis gated!postureEnforcesWall(...)and never runs under walled postures in this repo;last-admin-guardsees the same zero-grant-row population a fresh walled rig already has today before its owner registers (no new state); explain's provenance reads report nothing on empty tables;auto-org-admin-grantreads the permission-set rows, which this leg keeps minting (the org-less-row reap is L6, behind the census and auto-org-admin-grant resolves theorganization_adminset id by name alone (limit 1, unscoped, process-cached), so walled org-admin grants can point at the organization-less row #11670's resolver scoping). Nothing revoked on existing rigs — the legacy read is honoured.ensureDefaultOrganization, which still finds "the platform admin" via grant rows and triggers on grant inserts. A FRESH walled rig running that package will not auto-create its default organization between this leg landing and L3's re-point. The design's §6 dependency table imposes no L3-before-L4 order (both depend on L2 only), and existing rigs keep their rows; flagged here so the review weighs it consciously.email_verified: true, so config-derived standing works with zero fixture changes (packages/verify/src/harness.tsuntouched).Generated by Claude Code
Generated by Claude Code